Skip to content

Commit 819cfc4

Browse files
committed
cluster predicate - unidirectional graph reachability matrix
Questions/notes: 1. Is there an autoformatter? 2. I'm not sure about the `multiroot` suffix, should it be `anyroot`? 3. Also, is `cluster` the best name? Is `constellations`/`sccs` better? I needed this some time back, and having this would have saved me quite some time. Essentially, this ended up consisting of two parts: 1. `reachable_multiroot()` - a seemingly rather boringly straight-forward generalization of the `reachable()`, which, instead of enforcing that the subgraph is reachable from _the_ root node (given by index, non-optional), and thus forcing there to be a single joint subgraph, instead takes (zero or more) roots as a mask. The rest of the logic is generalized accordingly. 2. `cluster()` predicate. This one took **_quite_** a bit of rewrites to fully distill it to it's purest+simplest+fastest form. :) The (final) basic idea is to, basically, give each node a unique "color", and then set the nodes, linked together by selected edges, to the same color, thus ensuring that the nodes, that are not in any way connected, don't have the same "color", and vice versa. Implementation-wise, instead of color, for each node, we track the index of the disjoint subgraph to which this node belongs, and it turned out that tracking that by tracking the root node of each subgraph is the best approach (referred to as root). So there's 3 parts: 1. Splitting disjoint subgraphs: we then defer to `reachable_multiroot()` to enfore that each disjoint subgraph actually has a root node, thus different disjoint subgraphs have different roots. 2. Coalescing same-color subgraphs: nodes, that are linked together by selected edge, have the same root. 3. And then we actually compute reachability matrix based on whether or not two nodes have same root (belong to the same disjoint subgraph) **Now, this is controversial. I've decided to return the reachability matrix, *NOT* the subgraph index. While sparse data structure seems better, it immediately requires symmetry breaking constraint, which is tricky (i have written it.), and `--all-solutions` actually ends up being *MUCH* slower (`O^3`?) than just returning reachability matrix... The directed version would need to return the matrix, so this is also future-proofing for consistency sake** I have an exhaustive test harness for the `cluster()` (but not the rest), it would not have been possible to refine the predicate so much otherwise, please see https://github.com/LebedevRI/minizinc-graph-reachability-matrix-predicate.mzn/tree/proof-of-concept/unidirectional
1 parent 809109a commit 819cfc4

25 files changed

+13833
-0
lines changed

share/minizinc/std/cluster.mzn

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
include "fzn_cluster_int.mzn";
2+
include "fzn_cluster_int_reif.mzn";
3+
include "fzn_cluster_enum.mzn";
4+
include "fzn_cluster_enum_reif.mzn";
5+
6+
/** @group globals.graph
7+
Clusterize the subgraph \a ns and \a es of a given undirected graph into disjoint subgraphs,
8+
and produce a \a r node reachability matrix.
9+
10+
@param N: the number of nodes in the given graph
11+
@param E: the number of edges in the given graph
12+
@param from: the leaving node 1..\a N for each edge
13+
@param to: the entering node 1..\a N for each edge
14+
@param r: node reachability matrix
15+
@param ns: a Boolean for each node whether it is in the subgraph
16+
@param es: a Boolean for each edge whether it is in the subgraph
17+
*/
18+
predicate cluster(int: N, int: E, array[int] of int: from, array[int] of int: to,
19+
array[int,int] of var bool: r,
20+
array[int] of var bool: ns, array[int] of var bool: es) =
21+
assert(index_set(from) = 1..E,"cluster: index set of from must be 1..\(E)") /\
22+
assert(index_set(to) = 1..E,"cluster: index set of to must be 1..\(E)") /\
23+
assert(index_set(ns) = 1..N,"cluster: index set of ns must be 1..\(N)") /\
24+
assert(index_set(es) = 1..E,"cluster: index set of es must be 1..\(E)") /\
25+
fzn_cluster(N,E,from,to,r,ns,es);
26+
27+
/** @group globals.graph
28+
Clusterize the subgraph \a ns and \a es of a given undirected graph into disjoint subgraphs,
29+
and produce a \a r node reachability matrix.
30+
31+
@param from: the leaving node for each edge
32+
@param to: the entering node for each edge
33+
@param r: node reachability matrix
34+
@param ns: a Boolean for each node whether it is in the subgraph
35+
@param es: a Boolean for each edge whether it is in the subgraph
36+
*/
37+
predicate cluster(array[$$E] of $$N: from, array[$$E] of $$N: to,
38+
array[$$N,$$N] of var bool: r,
39+
array[$$N] of var bool: ns, array[$$E] of var bool: es) =
40+
let {
41+
int: NUM_NODES = card(index_set(ns));
42+
} in
43+
assert(index_set(from) = index_set(to),"cluster: index set of from and to must be identical") /\
44+
assert(index_set(from) = index_set(es),"cluster: index set of from and es must be identical") /\
45+
assert(index_set_1of2(r) = index_set_2of2(r),"cluster: nodes in from must be in index set of r") /\
46+
assert(dom_array(from) subset index_set_1of2(r),"cluster: nodes in from must be in index set of r") /\
47+
assert(dom_array(to) subset index_set_1of2(r),"cluster: nodes in to must be in index set of r") /\
48+
assert(dom_array(from) subset index_set(ns),"cluster: nodes in from must be in index set of ns") /\
49+
assert(dom_array(to) subset index_set(ns),"cluster: nodes in to must be in index set of ns") /\
50+
fzn_cluster(
51+
index2int(enum2int(from)),
52+
index2int(enum2int(to)),
53+
index2int(r),
54+
index2int(ns),
55+
index2int(es)
56+
);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
include "reachable_multiroot.mzn";
2+
3+
predicate fzn_cluster(array[$$E] of $$N: from, array[$$E] of $$N: to,
4+
array[$$N,$$N] of var bool: r,
5+
array[$$N] of var bool: ns, array[$$E] of var bool: es) =
6+
let {
7+
array[index_set(ns)] of var index_set(ns): NodeRoot;
8+
array[index_set(ns)] of var bool: NodeRootnessMask;
9+
array[index_set(ns)] of bool: ALL_GRAPH_NODES = [ true | n in index_set(ns) ];
10+
} in
11+
symmetry_breaking_constraint(forall (j,i in index_set(ns) where i > j)(
12+
r[i,j] == r[j,i]
13+
)) /\
14+
forall (c in index_set(ns))(
15+
r[c,c] == true
16+
) /\
17+
forall (j,i in index_set(ns) where i > j)(
18+
r[j,i] -> (ns[j] /\ ns[i])
19+
) /\
20+
reachable_multiroot(from,to,NodeRootnessMask,ALL_GRAPH_NODES,es) /\
21+
forall (n in index_set(ns))(
22+
NodeRootnessMask[n] == (NodeRoot[n] == n)
23+
) /\
24+
forall (e in index_set(es))(
25+
es[e] -> (NodeRoot[from[e]] == NodeRoot[to[e]])
26+
) /\
27+
forall (j,i in index_set(ns) where i > j)(
28+
r[j,i] == (NodeRoot[i] == NodeRoot[j])
29+
);
30+
31+
%-----------------------------------------------------------------------------%
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
predicate fzn_cluster_reif(array[$$E] of $$N: from, array[$$E] of $$N: to,
2+
array[$$N,$$N] of var bool: r, array[$$N] of var bool: ns,
3+
array[$$E] of var bool: es, var bool: b) =
4+
abort("Reified cluster constraint is not supported");
5+
6+
%-----------------------------------------------------------------------------%
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
include "reachable_multiroot.mzn";
2+
3+
predicate fzn_cluster(int: N, int: E, array[int] of int: from, array[int] of int: to,
4+
array[int,int] of var bool: r, array[int] of var bool: ns, array[int] of var bool: es) =
5+
let {
6+
array[1..N] of var 1..N: NodeRoot;
7+
array[1..N] of var bool: NodeRootnessMask;
8+
array[1..N] of bool: ALL_GRAPH_NODES = [ true | n in 1..N ];
9+
} in
10+
symmetry_breaking_constraint(forall (j,i in 1..N where i > j)(
11+
r[i,j] == r[j,i]
12+
)) /\
13+
forall (c in 1..N)(
14+
r[c,c] == true
15+
) /\
16+
forall (j,i in 1..N where i > j)(
17+
r[j,i] -> (ns[j] /\ ns[i])
18+
) /\
19+
reachable_multiroot(N,E,from,to,NodeRootnessMask,ALL_GRAPH_NODES,es) /\
20+
forall (n in 1..N)(
21+
NodeRootnessMask[n] == (NodeRoot[n] == n)
22+
) /\
23+
forall (e in 1..E)(
24+
es[e] -> (NodeRoot[from[e]] == NodeRoot[to[e]])
25+
) /\
26+
forall (j,i in 1..N where i > j)(
27+
r[j,i] == (NodeRoot[i] == NodeRoot[j])
28+
);
29+
30+
%-----------------------------------------------------------------------------%
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
predicate fzn_cluster_reif(int: N, int: E, array[int] of int: from, array[int] of int: to,
2+
array[int,int] of var bool: r, array[int] of var bool: ns,
3+
array[int] of var bool: es, var bool: b) =
4+
abort("Reified cluster constraint is not supported");
5+
6+
%-----------------------------------------------------------------------------%
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
include "subgraph.mzn";
2+
3+
predicate fzn_dreachable_multiroot(array[$$E] of $$N: from, array[$$E] of $$N: to,
4+
array[$$N] of var bool: rs, array[$$N] of var bool: ns, array[$$E] of var bool: es) =
5+
let {
6+
array[index_set(ns)] of var 0..card(index_set(ns))-1: dist; /* distance from root */
7+
array[index_set(ns)] of var index_set(ns): parent; /* parent */
8+
} in
9+
forall(n in index_set(ns)) % root nodes must be chosen
10+
(rs[n] -> ns[n]) /\
11+
forall(n in index_set(ns)) % self-parent is the same as either non-chosen node or root node
12+
((parent[n] = n) == ((not ns[n]) \/ rs[n])) /\
13+
forall(n in index_set(ns)) % each non-self-parent node is one more step removed from a root than its parent node
14+
(parent[n] != n -> dist[n] = dist[parent[n]] + 1) /\
15+
forall(n in index_set(ns)) % each non-self-parent node must have a chosen edge from its parent
16+
(parent[n] != n -> exists(e in index_set(from) where to[e] = n)(es[e] /\ from[e] = parent[n])) /\
17+
% nodes connected by chosen edges must be chosen
18+
subgraph(from,to,ns,es);
19+
20+
%-----------------------------------------------------------------------------%
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
predicate fzn_dreachable_multiroot_reif(array[$$E] of $$N: from, array[$$E] of $$N: to,
2+
array[$$N] of var bool: rs, array[$$N] of var bool: ns, array[$$E] of var bool: es,
3+
var bool: b) =
4+
abort("Reified dreachable_multiroot constraint is not supported");
5+
6+
%-----------------------------------------------------------------------------%
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
include "subgraph.mzn";
2+
3+
predicate fzn_dreachable_multiroot(int: N, int: E, array[int] of int: from, array[int] of int: to,
4+
array[int] of var bool: rs, array[int] of var bool: ns, array[int] of var bool: es) =
5+
let {
6+
array[1..N] of var 0..N-1: dist; /* distance from root */
7+
array[1..N] of var 1..N: parent; /* parent */
8+
} in
9+
forall(n in 1..N) % root nodes must be chosen
10+
(rs[n] -> ns[n]) /\
11+
forall(n in 1..N) % self-parent is the same as either non-chosen node or root node
12+
((parent[n] = n) == ((not ns[n]) \/ rs[n])) /\
13+
forall(n in 1..N) % each non-self-parent node is one more step removed from a root than its parent node
14+
(parent[n] != n -> dist[n] = dist[parent[n]] + 1) /\
15+
forall(n in 1..N) % each non-self-parent node must have a chosen edge from its parent
16+
(parent[n] != n -> exists(e in 1..E where to[e] = n)(es[e] /\ from[e] = parent[n])) /\
17+
% nodes connected by chosen edges must be chosen
18+
subgraph(N,E,from,to,ns,es);
19+
20+
%-----------------------------------------------------------------------------%
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
predicate fzn_dreachable_multiroot_reif(int: N, int: E, array[int] of int: from, array[int] of int: to,
2+
array[int] of var bool: rs, array[int] of var bool: ns, array[int] of var bool: es,
3+
var bool: b) =
4+
abort("Reified dreachable_multiroot constraint is not supported");
5+
6+
%-----------------------------------------------------------------------------%
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
include "subgraph.mzn";
2+
3+
predicate fzn_reachable_multiroot(array[$$E] of $$N: from, array[$$E] of $$N: to,
4+
array[$$N] of var bool: rs, array[$$N] of var bool: ns, array[$$E] of var bool: es) =
5+
let {
6+
int: E = length(es);
7+
set of int: NODE = index_set(ns);
8+
array[1..2*E] of NODE: dfrom = from ++ to;
9+
array[1..2*E] of NODE: dto = to ++ from;
10+
array[1..2*E] of var bool: des = es ++ es;
11+
} in
12+
/* duplicate the edges so that we can use directed graph reachability */
13+
fzn_dreachable_multiroot(dfrom,dto,rs,ns,des);
14+
15+
%-----------------------------------------------------------------------------%
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
predicate fzn_reachable_multiroot_reif(array[$$E] of $$N: from, array[$$E] of $$N: to,
2+
array[$$N] of var bool: rs, array[$$N] of var bool: ns, array[$$E] of var bool: es,
3+
var bool: b) =
4+
abort("Reified dreachable_multiroot constraint is not supported");
5+
6+
%-----------------------------------------------------------------------------%
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
include "subgraph.mzn";
2+
3+
predicate fzn_reachable_multiroot(int: N, int: E, array[int] of int: from, array[int] of int: to,
4+
array[int] of var bool: rs, array[int] of var bool: ns, array[int] of var bool: es) =
5+
let {
6+
array[1..2*E] of 1..N: dfrom = from ++ to;
7+
array[1..2*E] of 1..N: dto = to ++ from;
8+
array[1..2*E] of var bool: des = es ++ es;
9+
} in
10+
/* duplicate the edges so that we can use directed graph reachability */
11+
fzn_dreachable_multiroot(N,2*E,dfrom,dto,rs,ns,des);
12+
13+
%-----------------------------------------------------------------------------%
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
predicate fzn_reachable_multiroot_reif(int: N, int: E, array[int] of int: from, array[int] of int: to,
2+
array[int] of var bool: rs, array[int] of var bool: ns, array[int] of var bool: es,
3+
var bool: b) =
4+
abort("Reified dreachable_multiroot constraint is not supported");
5+
6+
%-----------------------------------------------------------------------------%

share/minizinc/std/globals.mzn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ include "bin_packing_load_fn.mzn";
6464
include "bounded_path.mzn";
6565
include "circuit.mzn";
6666
include "circuit_opt.mzn";
67+
include "cluster.mzn";
6768
include "connected.mzn";
6869
include "cost_mdd.mzn";
6970
include "cost_regular.mzn";
@@ -129,6 +130,7 @@ include "piecewise_linear_non_continuous.mzn";
129130
include "range.mzn";
130131
include "range_fn.mzn";
131132
include "reachable.mzn";
133+
include "reachable_multiroot.mzn";
132134
include "regular.mzn";
133135
include "regular_nfa.mzn";
134136
include "regular_regexp.mzn";
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
include "fzn_reachable_multiroot_int.mzn";
2+
include "fzn_reachable_multiroot_int_reif.mzn";
3+
include "fzn_reachable_multiroot_enum.mzn";
4+
include "fzn_reachable_multiroot_enum_reif.mzn";
5+
include "fzn_dreachable_multiroot_int.mzn";
6+
include "fzn_dreachable_multiroot_int_reif.mzn";
7+
include "fzn_dreachable_multiroot_enum.mzn";
8+
include "fzn_dreachable_multiroot_enum_reif.mzn";
9+
10+
/** @group globals.graph
11+
Constrains the subgraph \a ns and \a es of a given directed graph to be reachable from roots \a rs.
12+
13+
@param N: the number of nodes in the given graph
14+
@param E: the number of edges in the given graph
15+
@param from: the leaving node 1..\a N for each edge
16+
@param to: the entering node 1..\a N for each edge
17+
@param rs: a Boolean for each node whether it is a root node (which may be variable)
18+
@param ns: a Boolean for each node whether it is in the subgraph
19+
@param es: a Boolean for each edge whether it is in the subgraph
20+
*/
21+
predicate dreachable_multiroot(int: N, int: E, array[int] of int: from, array[int] of int: to,
22+
array[int] of var bool: rs, array[int] of var bool: ns, array[int] of var bool: es) =
23+
assert(index_set(from) = 1..E,"dreachable_multiroot: index set of from must be 1..\(E)") /\
24+
assert(index_set(to) = 1..E,"dreachable_multiroot: index set of to must be 1..\(E)") /\
25+
assert(index_set(rs) = 1..N,"dreachable_multiroot: index set of rs must be 1..\(N)") /\
26+
assert(index_set(ns) = 1..N,"dreachable_multiroot: index set of ns must be 1..\(N)") /\
27+
assert(index_set(es) = 1..E,"dreachable_multiroot: index set of es must be 1..\(E)") /\
28+
fzn_dreachable_multiroot(N,E,from,to,rs,ns,es);
29+
30+
/** @group globals.graph
31+
Constrains the subgraph \a ns and \a es of a given directed graph to be reachable from roots \a rs.
32+
33+
@param from: the leaving node for each edge
34+
@param to: the entering node for each edge
35+
@param rs: a Boolean for each node whether it is a root node (which may be variable)
36+
@param ns: a Boolean for each node whether it is in the subgraph
37+
@param es: a Boolean for each edge whether it is in the subgraph
38+
*/
39+
predicate dreachable_multiroot(array[$$E] of $$N: from, array[$$E] of $$N: to,
40+
array[$$N] of var bool: rs, array[$$N] of var bool: ns, array[$$E] of var bool: es) =
41+
assert(index_set(from) = index_set(to),"dreachable_multiroot: index set of from and to must be identical") /\
42+
assert(index_set(from) = index_set(es),"dreachable_multiroot: index set of from and es must be identical") /\
43+
assert(dom_array(from) subset index_set(rs),"dreachable_multiroot: nodes in from must be in index set of rs") /\
44+
assert(dom_array(to) subset index_set(rs),"dreachable_multiroot: nodes in to must be in index set of rs") /\
45+
assert(dom_array(from) subset index_set(ns),"dreachable_multiroot: nodes in from must be in index set of ns") /\
46+
assert(dom_array(to) subset index_set(ns),"dreachable_multiroot: nodes in to must be in index set of ns") /\
47+
fzn_dreachable_multiroot(
48+
index2int(enum2int(from)),
49+
index2int(enum2int(to)),
50+
index2int(rs),
51+
index2int(ns),
52+
index2int(es)
53+
);
54+
55+
%-----------------------------------------------------------------------------%
56+
57+
/** @group globals.graph
58+
Constrains the subgraph \a ns and \a es of a given undirected graph to be reachable from roots \a rs.
59+
60+
@param N: the number of nodes in the given graph
61+
@param E: the number of edges in the given graph
62+
@param from: the leaving node 1..\a N for each edge
63+
@param to: the entering node 1..\a N for each edge
64+
@param rs: a Boolean for each node whether it is a root node (which may be variable)
65+
@param ns: a Boolean for each node whether it is in the subgraph
66+
@param es: a Boolean for each edge whether it is in the subgraph
67+
*/
68+
predicate reachable_multiroot(int: N, int: E, array[int] of int: from, array[int] of int: to,
69+
array[int] of var bool: rs, array[int] of var bool: ns, array[int] of var bool: es) =
70+
assert(index_set(from) = 1..E,"reachable_multiroot: index set of from must be 1..\(E)") /\
71+
assert(index_set(to) = 1..E,"reachable_multiroot: index set of to must be 1..\(E)") /\
72+
assert(index_set(rs) = 1..N,"reachable_multiroot: index set of rs must be 1..\(N)") /\
73+
assert(index_set(ns) = 1..N,"reachable_multiroot: index set of ns must be 1..\(N)") /\
74+
assert(index_set(es) = 1..E,"reachable_multiroot: index set of es must be 1..\(E)") /\
75+
fzn_reachable_multiroot(N,E,from,to,rs,ns,es);
76+
77+
/** @group globals.graph
78+
Constrains the subgraph \a ns and \a es of a given undirected graph to be reachable from roots \a rs.
79+
80+
@param from: the leaving node for each edge
81+
@param to: the entering node for each edge
82+
@param rs: a Boolean for each node whether it is a root node (which may be variable)
83+
@param ns: a Boolean for each node whether it is in the subgraph
84+
@param es: a Boolean for each edge whether it is in the subgraph
85+
*/
86+
predicate reachable_multiroot(array[$$E] of $$N: from, array[$$E] of $$N: to,
87+
array[$$N] of var bool: rs, array[$$N] of var bool: ns, array[$$E] of var bool: es) =
88+
assert(index_set(from) = index_set(to),"reachable_multiroot: index set of from and to must be identical") /\
89+
assert(index_set(from) = index_set(es),"reachable_multiroot: index set of from and es must be identical") /\
90+
assert(dom_array(from) subset index_set(rs),"reachable_multiroot: nodes in from must be in index set of rs") /\
91+
assert(dom_array(to) subset index_set(rs),"reachable_multiroot: nodes in to must be in index set of rs") /\
92+
assert(dom_array(from) subset index_set(ns),"reachable_multiroot: nodes in from must be in index set of ns") /\
93+
assert(dom_array(to) subset index_set(ns),"reachable_multiroot: nodes in to must be in index set of ns") /\
94+
fzn_reachable_multiroot(
95+
index2int(enum2int(from)),
96+
index2int(enum2int(to)),
97+
index2int(rs),
98+
index2int(ns),
99+
index2int(es)
100+
);

0 commit comments

Comments
 (0)